package shell;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.Thread.State;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;
import javax.swing.UnsupportedLookAndFeelException;

import program.Shell;

import kernel.FileManager;
import kernel.IOManager;
import kernel.exception.OSException;
import kernel.process.AbstractProcessHeader;
import shell.parser.GrammarParser;

/**
 * GUI that represents a window/terminal of simulation
 * @author Jaromir Lepic
 *
 */
public class ShellWindow extends JFrame implements KeyListener, WindowListener {
	private static final long serialVersionUID = -7495673005750291533L;
	private static final int WIDTH = 640;
	private static final int HEIGHT = 480;
	private int historyPosition = -1;
	private GrammarParser parser;
	private JTextArea shell;
	private List<String> history = new ArrayList<String>();
	private Shell IO;
	private File lastWorkingDir;
	private int helpCycleIndex = 0;
	private List<String> suitablePrefixed = null;
	private boolean manualDispose = false;
	private STDInput stdinput = new STDInput();
	private Thread thread;
	private int shellBaseTextLength;
	
	/**
	 * Inner class that encapsulates variables related to standard input
	 * @author Jaromir Lepic
	 *
	 */
	private class STDInput{
		private boolean inputMode = false;
		private boolean inputMode2 = false;
		private StringBuffer messageBuffer;
		private Thread thread;
		private int shellBaseTextLength;
		private boolean hidden;
	}

	/**
	 * Creates new shell window
	 * @param workingDir Starting working directory
	 * @param thread Tread that runs this window
	 */
	public ShellWindow(File workingDir, Thread thread){
		this.thread = thread;
		lastWorkingDir = workingDir;
		try {
			for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
				if ("Nimbus".equals(info.getName())) {
					UIManager.setLookAndFeel(info.getClassName());
					break;
				}
			}
			if (!UIManager.getLookAndFeel().getName().equals("Nimbus")) 
				UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
		} catch (UnsupportedLookAndFeelException e) {
		} catch (ClassNotFoundException e) {
		} catch (InstantiationException e) {
		} catch (IllegalAccessException e) {
		}
		this.setPreferredSize(new Dimension(WIDTH, HEIGHT));
		this.setSize(new Dimension(WIDTH, HEIGHT));
		shell = new JTextArea();
		shell.addKeyListener(this);
		this.addWindowListener(this);
		for (MouseListener ML: shell.getMouseListeners()){
			shell.removeMouseListener(ML);
		}
		for (MouseMotionListener MML: shell.getMouseMotionListeners()){
			shell.removeMouseMotionListener(MML);
		}
		int mods = InputEvent.SHIFT_DOWN_MASK;
		int mods1 = InputEvent.CTRL_DOWN_MASK;
		int mods2 = InputEvent.ALT_DOWN_MASK;
		int mods3 = InputEvent.ALT_GRAPH_DOWN_MASK;
		
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "none");
		
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, mods), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, mods), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, mods), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, mods), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, mods), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, mods), "none");
		
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, mods1), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, mods1), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, mods1), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, mods1), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, mods1), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, mods1), "none");
		
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, mods2), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, mods2), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, mods2), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, mods2), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, mods2), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, mods2), "none");
		
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, mods3), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, mods3), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, mods3), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, mods3), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, mods3), "none");
		shell.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, mods3), "none");
		
		shell.setFont(new Font(shell.getFont().getName(), shell.getFont().getStyle(), 14));
		shell.setForeground(Color.GREEN);
		shell.setBackground(Color.black);
		shell.setHighlighter(null);
		shell.setLineWrap(true);
		this.setTitle("OS Simulation");
		JScrollPane jsp = new JScrollPane(shell);
		jsp.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "none");
		jsp.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "none");
		this.add(jsp);
		this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
		this.setVisible(true);
	}
	
	/**
	 * Formats working directory to be displayed
	 * @param dir Directory
	 * @return Formated directory
	 */
	private String formatPath (File dir){
		try {
			String path = URLDecoder.decode(dir.getCanonicalPath(), "UTF-8");
			if (path.charAt(0) == '\\' || path.charAt(0) == '/'){
				path = path.substring(1);
			}
			return path+">";
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
			return null;
		} catch (IOException e) {
			e.printStackTrace();
			return null;
		}
	}
	
	/**
	 * Sets new user
	 * @param userID
	 * @return Instance of this window
	 */
	public ShellWindow setUserID(String userID){
		parser.setUserID(userID);
		return this;
	}
	
	/**
	 * Sets new SHell to communicate with this window
	 * @param IO Given Shell
	 * @return Instance of this window
	 */
	public ShellWindow setIOClient(Shell IO){
		this.IO = IO;
		parser = new GrammarParser(IO.getID(), IO.getUserID());
		return this;
	}
	
	/**
	 * Removes N first lines from text area
	 * @param text Text to be shorteds
	 * @param number Number of lines to be removed
	 * @return Formated text
	 */
	public String removeNLines(String text, int number){
		for (int i = 0, n = 0; i < text.length(); i++){
			char c = text.charAt(i);
			if (c == '\n')
				n++;
			if (n == number){
				text = text.substring(i+1);
				break;
			}
		}
		return text;
	}
	
	/**
	 * Prints output to window
	 * @param outputMessage output to be printed
	 * @param workingDir new working directory to be set and used
	 */
	public void setOutput(OutputMessage outputMessage, File workingDir){
		lastWorkingDir = workingDir;
		String output = outputMessage.getMessage();
		String lineEnd;
		if (shell.getText().length() == 0)
			lineEnd = "";
		else 
			lineEnd = "\n";
		if (shell.getLineCount() > 100){
			shell.setText(removeNLines(shell.getText(), shell.getLineCount()-100));
		}
		if (!output.equals("")){
			shell.append(lineEnd+output);
		}
		if (outputMessage.isFinalMessage()){
			if (shell.getText().length() == 0){
				shell.append("");
			} else
			shell.append(lineEnd+formatPath(workingDir));
			shellBaseTextLength = shell.getText().length();
		}
		shell.setCaretPosition(shell.getText().length());
		if (outputMessage.hasSender()){
			outputMessage.getSender().interrupt();
		}
	}
	
	/**
	 * Gets standard input text from shell. Text is stored in given buffer and given thread
	 * is notified when job is done.
	 * @param messageBuffer Shared buffer
	 * @param thread Thread to be notified
	 */
	public void getTextFromShell(StringBuffer messageBuffer, Thread thread){
		shell.append("\n"+"User input required. Finish with CTRL+D:"+"\n");
		stdinput.messageBuffer = messageBuffer;
		stdinput.inputMode = true;
		stdinput.thread = thread;
		stdinput.shellBaseTextLength = shell.getText().length();
	}
	
	/**
	 * Gets standard input line from shell. Text is stored in given buffer and given thread
	 * is notified when job is done.
	 * @param messageBuffer Shared buffer
	 * @param message Message to be printed before line
	 * @param thread Thread to be notified
	 * @param hidden True if stars should be displayed instead
	 */
	public void getLineFromShell(StringBuffer messageBuffer, String message, Thread thread, boolean hidden){
		String lineEnd;
		if (shell.getText().length() == 0)
			lineEnd = "";
		else 
			lineEnd = "\n";
		shell.append(lineEnd + message);
		stdinput.messageBuffer = messageBuffer;
		stdinput.inputMode2 = true;
		stdinput.thread = thread;
		stdinput.shellBaseTextLength = shell.getText().length();
		stdinput.hidden = hidden;
	}

	@Override
	public void keyPressed(KeyEvent e) {
		boolean tabTyped = false;
		char typed = e.getKeyChar();
		if (typed == '\b') {
			if (stdinput.inputMode || stdinput.inputMode2){
				if (stdinput.shellBaseTextLength == shell.getText().length()){
					return;
				} else {
					String shellText1 = shell.getText().substring(0, shell.getCaretPosition() - 1);
					String shellText2 = shell.getText().substring(shell.getCaretPosition());
					shell.setText(shellText1+shellText2);
					shell.setCaretPosition(shellText1.length());
				}
			} else {
				if (shell.getCaretPosition() <= shellBaseTextLength){
					shell.setCaretPosition(shellBaseTextLength);
				} else {
					if (shell.getCaretPosition() == shell.getText().length()) {
						shell.setText(shell.getText().substring(0, shell.getText().length()-1));
					} else {
						String shellText1 = shell.getText().substring(0, shell.getCaretPosition() - 1);
						String shellText2 = shell.getText().substring(shell.getCaretPosition());
						shell.setText(shellText1+shellText2);
						shell.setCaretPosition(shellText1.length());
					}
				}
			}
		} else if (e.getKeyCode() == KeyEvent.VK_ENTER) {
			if (stdinput.inputMode){
				String shellText1 = shell.getText().substring(0, shell.getCaretPosition())+"\n";
				String shellText2 = shell.getText().substring(shell.getCaretPosition());
				shell.setText(shellText1+shellText2);
				shell.setCaretPosition(shellText1.length());
			} else if (stdinput.inputMode2){
				stdinput.inputMode2 = false;
				stdinput.messageBuffer.append(shell.getText().substring(stdinput.shellBaseTextLength));
				stdinput.hidden = false;
				synchronized (stdinput.thread) {
					stdinput.thread.notify();
				}
			} else {
				String command = (shell.getText().substring(shellBaseTextLength, shell.getText().length()));
				if (!command.equals("") && !command.trim().equals("")) {
					history.add(command);
					historyPosition = history.size()-1;
					AbstractProcessHeader[] headers;
					try {
						headers = parser.parseCommand(command);
						IO.getMessageQueue().add(headers);
					} catch (OSException e1) {
						setOutput(new OutputMessage(e1.getMessage(), true), lastWorkingDir);
					}
					synchronized (IO.getThread()) {
						if (IO.getThread().getState().equals(State.WAITING))
							IO.getThread().notify();
					}
				} else {
					setOutput(new OutputMessage("", true), lastWorkingDir);
				}
			}
		} else if (e.getKeyCode() == KeyEvent.VK_TAB) {
			if (stdinput.inputMode || stdinput.inputMode2 ){
				String shellText1 = shell.getText().substring(0, shell.getCaretPosition())+"\t";
				String shellText2 = shell.getText().substring(shell.getCaretPosition());
				shell.setText(shellText1+shellText2);
				shell.setCaretPosition(shellText1.length());
			} else {
				tabTyped = true;
				try {
					String command = (shell.getText().substring(shellBaseTextLength, 
							shell.getCaretPosition()));
					List<String> words = parser.parseToStrings(command);
					if (words.size() > 1){
						if (words.get(words.size()-2).equals("|")) return;
						if (suitablePrefixed == null){
							String[] files = FileManager.getDirContent(lastWorkingDir, false);
							String prefix = words.get(words.size() -1).toLowerCase();
							suitablePrefixed = new ArrayList<String>();
							for (String file: files){
								if (file.toLowerCase().startsWith(prefix)){
									if (file.split(" ").length > 1){
										file = "\"" + file + "\"";
									}
									suitablePrefixed.add(file);
								}
							}
						}
						if (suitablePrefixed.size() > 0){
							String baseText = shell.getText().substring(0, shellBaseTextLength);
							String newText = "";
							for (int i = 0; i < words.size() -1; i++){
								newText += words.get(i) + " ";
							}
							newText += suitablePrefixed.get(helpCycleIndex);
							shell.setText(baseText+newText);
							helpCycleIndex = (helpCycleIndex + 1) % suitablePrefixed.size();
						}
					}
				} catch (OSException e1) {
				}
			}
		} else if (e.getKeyCode() == KeyEvent.VK_UP) {
			if (stdinput.inputMode || stdinput.inputMode2){
			} else {
					if (history.size() > 0 && historyPosition >= 0){
						shell.setText(shell.getText().substring(0, shellBaseTextLength));
						String command = history.get(historyPosition--);
						shell.append(command);
					}
			}
		} else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
			if (stdinput.inputMode || stdinput.inputMode2){
			} else {
				if (history.size() > 0 && historyPosition < history.size() -2){
					shell.setText(shell.getText().substring(0, shellBaseTextLength));
					historyPosition ++;
					String command = history.get(historyPosition+1);
					shell.append(command);
				}
			}
		} else if (e.getKeyCode() == KeyEvent.VK_LEFT){
			if (stdinput.inputMode || stdinput.inputMode2){
				if (shell.getCaretPosition() > stdinput.shellBaseTextLength){
					shell.setCaretPosition(shell.getCaretPosition() - 1);
				}
			} else {
				if (shell.getCaretPosition() > shellBaseTextLength){
					shell.setCaretPosition(shell.getCaretPosition() - 1);
				}
			}
		} else if (e.getKeyCode() == KeyEvent.VK_RIGHT){
		}
		else if (e.getKeyCode() == KeyEvent.VK_DELETE){
			if (!stdinput.inputMode && !stdinput.inputMode2){
			}
		}
		else if (e.getKeyCode() == KeyEvent.VK_D && e.isControlDown()){
			if (stdinput.inputMode){
				stdinput.inputMode = false;
				stdinput.messageBuffer.append(shell.getText().substring(stdinput.shellBaseTextLength));
				synchronized (stdinput.thread) {
					stdinput.thread.notify();
				}
			}
		}
		else if (isPrintableChar(typed)) {
			if (!stdinput.inputMode && !stdinput.inputMode2){
			}
			if (stdinput.hidden){
				String shellText1 = shell.getText().substring(0, shell.getCaretPosition())+"*";
				String shellText2 = shell.getText().substring(shell.getCaretPosition());
				shell.setText(shellText1+shellText2);
				shell.setCaretPosition(shellText1.length());
			}
		}
		
		if (!tabTyped){
			helpCycleIndex = 0;
			suitablePrefixed = null;
		}
		
	}

	@Override
	public void keyReleased(KeyEvent e) {
		
		
	}

	@Override
	public void keyTyped(KeyEvent e) {
		if (stdinput.hidden){
			e.consume();
		}
	}
	
	/**
	 * Tests if char is printable
	 * @param c Tested char
	 * @return True if printable
	 */
	private boolean isPrintableChar( char c ) {
	    Character.UnicodeBlock block = Character.UnicodeBlock.of( c );
	    return (!Character.isISOControl(c)) &&
	            c != KeyEvent.CHAR_UNDEFINED &&
	            block != null &&
	            block != Character.UnicodeBlock.SPECIALS;
	}
	
	/**
	 * Manually calls the dispose method
	 */
	public void disposeManually(){
		if (thread.isAlive()){
			thread.interrupt();
		}
		manualDispose = true;
		this.dispose();
	}

	@Override
	public void windowActivated(WindowEvent arg0) {
	}

	@Override
	public void windowClosed(WindowEvent arg0) {
		IOManager.getInstance().shellClosing(IO.getID(), manualDispose);
	}

	@Override
	public void windowClosing(WindowEvent arg0) {
	}

	@Override
	public void windowDeactivated(WindowEvent arg0) {
	}

	@Override
	public void windowDeiconified(WindowEvent arg0) {
	}

	@Override
	public void windowIconified(WindowEvent arg0) {
	}

	@Override
	public void windowOpened(WindowEvent arg0) {
	}

}
